﻿namespace Baddies.Map
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Security;
    using System.Text;   
    using Baddies.Events;
    using Baddies.Nodes;
    using Baddies.Scenes;
    using Baddies.Utils;    
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Content;
    using Microsoft.Xna.Framework.Graphics;            

    /// <summary>
    /// A grid of tiles to represent the visual map of a zone. 
    /// Keeps track of floor tiles and scenario objects. 
    /// </summary>    
    public class MapGrid : Node, IContentHolder
    {
        /// <summary>
        /// Size of the map tiles.
        /// </summary>
        private int tileSize;

        /// <summary>
        /// Number of colums of tiles in the map.
        /// </summary>
        private int mapWidth;

        /// <summary>
        /// Number of rows in the map.
        /// </summary>
        private int mapHeight;

        /// <summary>
        /// Matrix of tiles that form the map.
        /// As a list-list matrix instead of children node for optimization purposes.
        /// </summary>
        private List<MapTile> tiles;

        /// <summary>
        /// List of tiles to draw, that have survived culling.
        /// </summary>
        private List<MapTile> tilesToDraw;

        /// <summary>
        /// Spritemap terrain of all the textures in this map.
        /// </summary>
        [ContentSerializer]
        private Texture2DProxy tileSetTex;

        /// <summary>
        /// Indicates whether to draw the collision map.
        /// </summary>
        private bool drawCollisionMap;

        /// <summary>
        /// Indicates whether to draw the map grid.
        /// </summary>
        private bool drawGrid;

        /// <summary>
        /// Indicates whether to draw the terrain tiles.
        /// </summary>
        private bool drawTerrain;

        /// <summary>
        /// Texture to use to draw the grid.
        /// </summary>
        private Texture2D gridTexture;

        /// <summary>
        /// Gets or sets a value indicating whether to draw the terrain.
        /// </summary>
        /// <value>True if we want to draw, false otherwise.</value>
        public bool DisplayTerrain
        {
            set { this.drawTerrain = value; }
            get { return this.drawTerrain; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether to draw the grid.
        /// </summary>
        /// <value>True if we want to draw, false otherwise.</value>
        public bool DisplayGrid
        {
            set { this.drawGrid = value; }
            get { return this.drawGrid; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether to draw the collision map.
        /// </summary>
        /// <value>True if we want to draw, false otherwise.</value>
        public bool DisplayCollisionMap
        {
            set { this.drawCollisionMap = value; }
            get { return this.drawCollisionMap; }
        }

        /// <summary>
        /// Gets or sets the number of cols in the map.
        /// </summary>
        /// <value>The width of the map in cols.</value>
        public int MapWidth
        {
            get { return this.mapWidth; }
            set { this.mapWidth = value; }
        }

        /// <summary>
        /// Gets or sets the number of rows in the map.
        /// </summary>
        /// <value>Height of the map in rows.</value>
        public int MapHeight
        {
            get { return this.mapHeight; }
            set { this.mapHeight = value; }
        }

        /// <summary>
        /// Gets or sets the size of a tile.
        /// </summary>
        /// <value>The size of a tile.</value>
        public int TileSize
        {
            get { return this.tileSize; }
            set { this.tileSize = value; }
        }

        /// <summary>
        /// Gets the tiles list.
        /// </summary>
        /// <value>List of tiles that form the map.</value>
        public List<MapTile> Tiles
        {
            get { return this.tiles; }
        }

        /// <summary>
        /// Gets or sets a tile in the map. Indexer for quick tile access.
        /// </summary>
        /// <param name="x">Column of the tile.</param>
        /// <param name="y">Row of the tile.</param>
        /// <returns>The specified tile.</returns>
        public MapTile this[int x, int y]
        {
            get 
            {
                Debug.Assert(x >= 0 && x < this.mapWidth, "Requested x ( " + x + " ) is out fo range");
                Debug.Assert(y >= 0 && y < this.mapHeight, "Requested y ( " + y + " ) is out fo range");
                return this.tiles[this.Linear(x, y)]; 
            }

            set 
            {
                this.tiles[this.Linear(x, y)] = value; 
            }
        }

        /// <summary>
        /// Initializes a new instance of the MapGrid class.
        /// </summary>
        public MapGrid()
            : base()
        {
            this.mapHeight = 0;
            this.mapWidth = 0;
            this.tileSize = 0;
            this.drawCollisionMap = false;
            this.drawGrid = false;
            this.drawTerrain = true;
            this.tileSetTex = new Texture2DProxy(this);
            this.tiles = new List<MapTile>();
            this.tilesToDraw = new List<MapTile>();

            // Create grid texture.
            GetGameGfxDeviceArg args = new GetGameGfxDeviceArg();
            EventManager.GetEV.Trigger("GetGameGfxDevice", this, args);
            this.gridTexture = new Texture2D(args.GraphicsDevice, 1, 1);
            this.gridTexture.SetData(new[] { Color.Red });
        }

        /// <summary>
        /// Changes from matrix fromat to linear.
        /// </summary>
        /// <param name="x">Column of the tile.</param>
        /// <param name="y">Row of the tile.</param>
        /// <returns>Index of the tile in the tiles list.</returns>
        private int Linear(int x, int y)
        {
            return (y * this.mapWidth) + x;
        }

        /// <summary>
        /// Checks whether a specific tile position exists.
        /// </summary>
        /// <param name="x">Column of the tile.</param>
        /// <param name="y">Row of the tile.</param>
        /// <returns>True if it exists, false if it's out of range.</returns>
        public bool Exists(int x, int y)
        {
            if (x < 0 || x >= this.mapWidth)
            {
                return false;
            }

            if (y < 0 || y >= this.mapHeight)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Implements the IContentHolder interface.
        /// </summary>
        /// <returns>Gets the ContentManager that MapGrid uses.</returns>
        public ContentManager GetContent()
        {
            return Scene.Content;
        }

        /// <summary>
        /// Creates a new empty map.
        /// </summary>
        /// <param name="width">Width of the new map in tiles.</param>
        /// <param name="height">Height of the new map in tiles.</param>
        /// <param name="tileSize">Size of the tiles.</param>
        public void CreateNewMap(int width, int height, int tileSize)
        {
            this.mapHeight = height;
            this.mapWidth = width;
            this.tileSize = tileSize;

            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {                         
                    MapTile tile = new MapTile(x, y, tileSize);
                    this.tiles.Add(tile); 
                }                
            }
        }

        /// <summary>
        /// Sets the name and texture for the terrain map.
        /// To be used by the game editor.
        /// </summary>
        /// <param name="tileSetName">Name of the terrain file, pure name only, no paths. 
        /// It asumes the file is in the Content/TileMap directory.</param>
        /// <param name="tileSet">Reference to the tileset texture.</param>
        public void SetTileset(string tileSetName, Texture2D tileSet)
        {
            this.tileSetTex.Name = "TileMap/" + tileSetName;            
            this.tileSetTex.Texture = tileSet; 
        }

        /// <summary>
        /// Given a real position (in pixels), it returns the tile in that point.
        /// </summary>
        /// <param name="x">X position in pixels.</param>
        /// <param name="y">Y position in pixels.</param>
        /// <returns>Tile in that position.</returns>
        public MapTile GetTileFromRealPos(int x, int y)
        {
            int mapX = x / this.tileSize;
            int mapY = y / this.tileSize;
            Debug.Assert(mapX < this.mapWidth, "X out of range! x = (" + x + " / " + (x / this.tileSize) + ") !< " + this.mapWidth);
            Debug.Assert(mapY < this.mapHeight, "Y out of range! y = (" + y + " / " + (y / this.tileSize) + ") !< " + this.mapHeight);
            return this[mapX, mapY];
        }

        #region Drawing
        /// <summary>
        /// Draws the MapGrid.
        /// </summary>
        /// <param name="spritebatch">Spritebatch to draw on.</param>
        public override void Draw(SpriteBatch spritebatch)
        {
            base.Draw(spritebatch);

            if (this.drawTerrain)
            {
                this.DrawTerrain(spritebatch);
            }

            if (this.drawCollisionMap)
            {
                this.DrawCollisionMap(spritebatch);
            }

            if (this.drawGrid)
            {
                this.DrawTileGrid(spritebatch);
            }
        }                

        /// <summary>
        /// Culls the tiles that lie outside the camera view.
        /// </summary>
        /// <param name="camera">Camera to check against.</param>
        /// <returns>False, as the MapGrid itself does not need to be culled.</returns>
        public override bool Cull(Camera camera)
        {
            this.tilesToDraw.Clear();

            // Just to iterate inside the camera view and not bother 
            // with tiles outside the camera
            int left, right, top, down;
            left = Math.Max((int)camera.Position.X / this.tileSize, 0);
            right = Math.Min(((int)camera.Position.X + camera.W) / this.tileSize, this.mapWidth);
            top = Math.Max((int)camera.Position.Y / this.tileSize, 0);
            down = Math.Min(((int)camera.Position.Y + camera.H) / this.tileSize, this.mapHeight);

            for (int i = top; i < down; i++)
            {
                for (int j = left; j < right; j++)
                {                
                    this.tilesToDraw.Add(this[j, i]);                    
                }
            }

            // No need to cull the map itself as if all the tiles 
            // lie outside the map nothing will be drawn anyhow.
            return false;
        }

        /// <summary>
        /// Draws the terrain map.
        /// </summary>
        /// <param name="spritebatch">Spritebatch to draw on.</param>
        public void DrawTerrain(SpriteBatch spritebatch)
        {
            if (this.tileSetTex.Texture != null)
            {
                foreach (MapTile tile in this.tilesToDraw)
                {
                    tile.DrawTerrain(spritebatch, this.tileSetTex, this.DrawPosition);
                }
            }
        }

        /// <summary>
        /// Draws the collision map.
        /// </summary>
        /// <param name="spritebatch">Spritebatch to draw on.</param>
        public void DrawCollisionMap(SpriteBatch spritebatch)
        {
            foreach (MapTile tile in this.tilesToDraw)
            {
                tile.DrawCollision(spritebatch, this.DrawPosition);
            }
        }

        /// <summary>
        /// Draws an auxiliary tile-grid to see the different tiles.
        /// </summary>
        /// <param name="spritebatch">Spritebatch to draw on.</param>
        public void DrawTileGrid(SpriteBatch spritebatch)
        {
            foreach (MapTile tile in this.tilesToDraw)
            {
                tile.DrawGrid(spritebatch, this.gridTexture, this.DrawPosition);
            }
        }
        #endregion // Drawing

        #region Size modification
        /// <summary>
        /// Adds a number of lines to one of the 4 sides of the map.
        /// </summary>
        /// <param name="sideofMap">Side of the map to add lines on.</param>
        /// <param name="numberLines">Number of lines to add.</param>
        public void AddLines(Util.Dir sideofMap, int numberLines)
        {
            switch (sideofMap)
            {
                case Util.Dir.Down:
                    {
                        int numberTiles = this.mapWidth * numberLines;
                        int height = this.mapHeight + numberLines;
                        int width = this.mapWidth;

                        for (int y = this.mapHeight; y < height; y++)
                        {
                            for (int x = 0; x < width; x++)
                            {
                                MapTile tile = new MapTile(x, y, this.tileSize);
                                this.tiles.Add(tile);
                            }
                        }

                        this.mapHeight += numberLines;
                    }

                    break;

                case Util.Dir.Up:
                    {
                        // Create the lines of new tiles on top.
                        int numberTiles = this.mapWidth * numberLines;
                        int height = numberLines;
                        int width = this.mapWidth;
                        List<MapTile> aux = new List<MapTile>();
                        for (int y = 0; y < height; y++)
                        {
                            for (int x = 0; x < width; x++)
                            {
                                MapTile tile = new MapTile(x, y, this.tileSize);
                                aux.Add(tile);
                            }
                        }

                        // Push all the old tiles down.
                        for (int y = 0; y < this.mapHeight; y++)
                        {
                            for (int x = 0; x < this.mapWidth; x++)
                            {
                                this[x, y].SetMapPosition(x, y + numberLines, this.tileSize);
                            }
                        }

                        // Add the new tiles.
                        this.tiles.InsertRange(0, aux);
                        this.mapHeight += numberLines;
                    }

                    break;

                case Util.Dir.Left:
                    {
                        // Push all the old tiles right
                        for (int y = 0; y < this.mapHeight; y++)
                        {
                            for (int x = 0; x < this.mapWidth; x++)
                            {
                                this[x, y].SetMapPosition(x + numberLines, y, this.tileSize);
                            }
                        }

                        // Add the new lines                        
                        for (int y = 0; y < this.mapHeight; y++)
                        {
                            for (int x = 0; x < numberLines; x++)
                            {
                                this.tiles.Insert(
                                    this.Linear(x, y) + (numberLines * y),
                                    new MapTile(x, y, this.tileSize));
                            }
                        }

                        // Change the width
                        this.mapWidth += numberLines; 
                    }

                    break;

                case Util.Dir.Right:
                    {
                        // Add the new lines
                        for (int y = 0; y < this.mapHeight; y++)
                        {
                            for (int x = this.mapWidth; x < this.mapWidth + numberLines; x++)
                            {
                                this.tiles.Insert(
                                    this.Linear(x, y) + (numberLines * y),
                                    new MapTile(x, y, this.tileSize));
                            }
                        }

                        // Change the width
                        this.mapWidth += numberLines;
                    }

                    break;
            }
        }

        /// <summary>
        /// Adds a number of lines to one of the 4 sides of the map.
        /// </summary>
        /// <param name="sideofMap">Side of the map to add lines on.</param>
        /// <param name="numberLines">Number of lines to add.</param>
        public void RemoveLines(Util.Dir sideofMap, int numberLines)
        {
            switch (sideofMap)
            {
                case Util.Dir.Down:
                    {
                        // Don't allow to errase more lines that there are
                        if (-numberLines > this.mapHeight)
                        {
                            numberLines =  -this.mapHeight;
                        }

                        // Remove lines
                        int index = this.mapWidth * (this.mapHeight + numberLines);
                        int count = (-numberLines) * this.mapWidth;
                        this.tiles.RemoveRange(index, count);

                        // Reduce map size.
                        this.mapHeight += numberLines;
                    }

                    break;

                case Util.Dir.Up:
                    {
                        // Don't allow to errase more lines that there are
                        if (-numberLines > this.mapHeight)
                        {
                            numberLines = -this.mapHeight;
                        }

                        // Remove lines
                        int index = 0;
                        int count = -numberLines * this.mapWidth;
                        this.tiles.RemoveRange(index, count);

                        // Update indexes, pulling all the tiles up
                        for (int y = -numberLines; y < this.mapHeight; y++)
                        {
                            for (int x = 0; x < this.mapWidth; x++)
                            {
                                this[x, y + numberLines].SetMapPosition(x, y + numberLines, this.tileSize);
                            }
                        }

                        // Reduce map size.
                        this.mapHeight += numberLines;
                    }

                    break;

                case Util.Dir.Left:
                    {
                        // Don't allow to errase more lines that there are
                        if (-numberLines > this.mapWidth)
                        {
                            numberLines = -this.mapWidth;
                        }

                        // Remove lines starting from the bottom.
                        for (int y = this.mapHeight - 1; y >= 0; y--)
                        {
                            for (int x = -numberLines - 1; x >= 0; x--)
                            {
                                this.tiles.RemoveAt(this.Linear(x, y)); 
                            }
                        }

                        // Reduce map size.
                        this.mapWidth += numberLines;
                        
                        // Update indexes, pulling all the tiles up
                        for (int y = 0; y < this.mapHeight; y++)
                        {
                            for (int x = 0; x < this.mapWidth; x++)
                            {
                                this[x, y].SetMapPosition(x, y, this.tileSize);
                            }
                        }                        
                    }

                    break;

                case Util.Dir.Right:
                    {
                        // Don't allow to errase more lines that there are
                        if (-numberLines > this.mapWidth)
                        {
                            numberLines = -this.mapWidth;
                        }

                        // Remove lines starting from the bottom.
                        for (int y = this.mapHeight - 1; y >= 0; y--)
                        {
                            for (int x = this.mapWidth - 1; x >= this.mapWidth + numberLines; x--)
                            {
                                this.tiles.RemoveAt(this.Linear(x, y));
                            }
                        }

                        // Reduce map size.
                        this.mapWidth += numberLines;
                    }

                    break;
            }
        }

        #endregion // Size modification
    }
}
